package it.unibo.refolding.pico.stripchart;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.LinearFunction;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.pico.LoopParams;
import it.unibo.refolding.pico.PicoDataAnalyser;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import it.unimi.dsi.fastutil.doubles.DoubleList;
import it.unimi.dsi.fastutil.doubles.DoubleLists;

import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.lang3.StringUtils;

import com.google.common.base.Function;
import com.google.common.base.Functions;

public class AnalysisOptions {
  private final Option fetchRefoldBreakParamsOption, svgDirOption, infoDirOption, minPeakForcePnOption,
    minPeakContourLengthNmOption, minContourLengthDeltaNmOption,
    minPeakForceRelativeToEmpiricalBaselinePnOption, medianAbsoluteDeviationLowerBoundPnOption,
    leftMostlyHigherWindowSizeNmOption, leftMostlyHigherFractionOption, leftMostlyHigherPnOption,
    rightMuchHigherThanAllPnOption, rightMuchHigherPnOption, atMostOnePeakWindowNmOption,
    linkerNmOption, linkerPerModuleNmOption, maxRequiredWellSpacedAtEndOption, temperatureKOption,
    persistenceLengthNmOption;

  private AnalysisOptions(Option paramsOption, Option svgDirOption, Option infoDirOption,
    Option minPeakForcePnOption, Option minPeakContourLengthNmOption,
    Option minContourLengthDeltaNmOption, Option minPeakForceRelativeToEmpiricalBaselinePnOption,
    Option medianAbsoluteDeviationLowerBoundPnOption,
    Option leftMostlyHigherWindowSizeNmOption,
    Option leftMostlyHigherFractionOption,
    Option leftMostlyHigherPnOption,
    Option rightMuchHigherThanAllPnOption, Option rightMuchHigherPnOption, Option atMostOnePeakWindowNmOption, Option linkerNmOption,
    Option linkerPerModuleNmOption, Option maxRequiredWellSpacedAtEndOption,
    Option temperatureKOption, Option persistenceLengthNmOption) {
    this.fetchRefoldBreakParamsOption = paramsOption;
    this.svgDirOption = svgDirOption;
    this.infoDirOption = infoDirOption;
    this.minPeakForcePnOption = minPeakForcePnOption;
    this.minPeakContourLengthNmOption = minPeakContourLengthNmOption;
    this.minContourLengthDeltaNmOption = minContourLengthDeltaNmOption;
    this.minPeakForceRelativeToEmpiricalBaselinePnOption = minPeakForceRelativeToEmpiricalBaselinePnOption;
    this.medianAbsoluteDeviationLowerBoundPnOption = medianAbsoluteDeviationLowerBoundPnOption;
    this.leftMostlyHigherWindowSizeNmOption = leftMostlyHigherWindowSizeNmOption;
    this.leftMostlyHigherFractionOption = leftMostlyHigherFractionOption;
    this.leftMostlyHigherPnOption = leftMostlyHigherPnOption;
    this.rightMuchHigherThanAllPnOption = rightMuchHigherThanAllPnOption;
    this.rightMuchHigherPnOption = rightMuchHigherPnOption;
    this.atMostOnePeakWindowNmOption = atMostOnePeakWindowNmOption;
    this.linkerNmOption = linkerNmOption;
    this.linkerPerModuleNmOption = linkerPerModuleNmOption;
    this.maxRequiredWellSpacedAtEndOption = maxRequiredWellSpacedAtEndOption;
    this.temperatureKOption = temperatureKOption;
    this.persistenceLengthNmOption = persistenceLengthNmOption;
  }

  public void addTo(Options options) {
    for (Field field : getClass().getDeclaredFields()) {
      Option option;
      try {
        option = (Option) field.get(this);
      } catch (IllegalAccessException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
      options.addOption(option);
    }
  }

  public static AnalysisOptions create() {
    Option fetchRefoldBreakParamsOption = createFetchRefoldBreakParamsOption();

    Option svgDirOption = createSvgDirOption();

    Option infoDirOption = new Option(
      "infoDir", true, "where to save debug and/or refolding svg files");
    infoDirOption.setRequired(true);
    infoDirOption.setType(String.class);

    Option minPeakForcePnOption = createMinPeakForcePnOption();
    Option minPeakContourLengthNmOption = createMinPeakContourLengthNmOption();
    Option minContourLengthDeltaNmOption = createMinContourLengthDeltaNmOption();
    Option minPeakForceRelativeToEmpiricalBaselinePnOption = createMinPeakForceRelativeToEmpiricalBaselinePnOption();
    Option medianAbsoluteDeviationLowerBoundPnOption = createMedianAbsoluteDeviationLowerBoundPnOption();
    Option leftMostlyHigherWindowSizeNmOption = createLeftMostlyHigherWindowSizeNmOption();
    Option leftMostlyHigherFractionOption = createLeftMostlyHigherFractionOption();
    Option leftMostlyHigherPnOption = createLeftMostlyHigherPnOption();
    Option rightMuchHigherThanAllPnOption = createRightMuchHigherThanAllPnOption();
    Option rightMuchHigherPnOption = createRightMuchHigherPnOption();
    Option atMostOnePeakWindowNmOption = createAtMostOnePeakWindowNmOption();

    Option linkerNmOption = new Option("linkerNm", true, "fixed linker [nm].");
    linkerNmOption.setRequired(true);
    linkerNmOption.setType(Double.TYPE);

    Option linkerPerModuleNmOption = new Option("linkerPerModuleNm", true, "fixed linker [nm].");
    linkerPerModuleNmOption.setRequired(true);
    linkerPerModuleNmOption.setType(Double.TYPE);

    Option maxRequiredWellSpacedAtEndOption = new Option(
      "maxRequiredWellSpacedAtEnd", true, "fixed linker [nm].");
    maxRequiredWellSpacedAtEndOption.setRequired(true);
    maxRequiredWellSpacedAtEndOption.setType(Integer.TYPE);

    Option temperatureKOption = createTemperatureKOption();
    Option persistenceLengthNmOption = createPersistenceLengthNmOption();

    return new AnalysisOptions(
      fetchRefoldBreakParamsOption, svgDirOption, infoDirOption, minPeakForcePnOption,
      minPeakContourLengthNmOption, minContourLengthDeltaNmOption,
      minPeakForceRelativeToEmpiricalBaselinePnOption, medianAbsoluteDeviationLowerBoundPnOption,
      leftMostlyHigherWindowSizeNmOption, leftMostlyHigherFractionOption, leftMostlyHigherPnOption,
      rightMuchHigherThanAllPnOption, rightMuchHigherPnOption, atMostOnePeakWindowNmOption,
      linkerNmOption, linkerPerModuleNmOption, maxRequiredWellSpacedAtEndOption,
      temperatureKOption, persistenceLengthNmOption);
  }

  public static Option createFetchRefoldBreakParamsOption() {
    Option fetchRefoldBreakParamsOption = new Option(
      "fetchRefoldBreakParams", true, "fetch/refold/break configuration");
    fetchRefoldBreakParamsOption.setRequired(true);
    fetchRefoldBreakParamsOption.setType(String.class);
    return fetchRefoldBreakParamsOption;
  }

  public static Option createLoopParamsOption() {
    Option fetchRefoldBreakParamsOption = new Option(
      "loopParams", true, "loop configuration");
    fetchRefoldBreakParamsOption.setRequired(true);
    fetchRefoldBreakParamsOption.setType(String.class);
    return fetchRefoldBreakParamsOption;
  }

  public static Option createTemperatureKOption() {
    Option temperatureKOption = new Option("temperatureK", true, "");
    temperatureKOption.setRequired(true);
    temperatureKOption.setType(Double.TYPE);
    return temperatureKOption;
  }

  public static Option createPersistenceLengthNmOption() {
    Option persistenceLengthNmOption = new Option(
      "persistenceLengthNm", true, "fixed persistenceLength [nm].");
    persistenceLengthNmOption.setRequired(true);
    persistenceLengthNmOption.setType(Double.TYPE);
    return persistenceLengthNmOption;
  }

  public static Option createSvgDirOption() {
    Option svgDirOption = new Option(
      "svgDir", true, "where to save debug and/or refolding svg files");
    svgDirOption.setRequired(true);
    svgDirOption.setType(String.class);
    return svgDirOption;
  }

  public static Option createMinPeakForcePnOption() {
    Option minPeakForcePnOption = new Option("minPeakForcePn", true, "Min peak force [pN].");
    minPeakForcePnOption.setRequired(false);
    minPeakForcePnOption.setType(Double.TYPE);
    return minPeakForcePnOption;
  }

  public static Option createMinPeakContourLengthNmOption() {
    Option minPeakContourLengthNmOption = new Option(
      "minPeakContourLengthNm", true, "Min peak contour length [nm].");
    minPeakContourLengthNmOption.setRequired(true);
    minPeakContourLengthNmOption.setType(Double.TYPE);
    return minPeakContourLengthNmOption;
  }

  public static Option createMinContourLengthDeltaNmOption() {
    Option minContourLengthDeltaNmOption = new Option(
      "minContourLengthDeltaNm", true, "Min contour length increase from peak to peak [nm].");
    minContourLengthDeltaNmOption.setRequired(true);
    minContourLengthDeltaNmOption.setType(Double.TYPE);
    return minContourLengthDeltaNmOption;
  }

  public static Option createAtMostOnePeakWindowNmOption() {
    Option atMostOnePeakWindowNmOption = new Option("atMostOnePeakWindowNm", true, "");
    atMostOnePeakWindowNmOption.setRequired(true);
    atMostOnePeakWindowNmOption.setType(Double.TYPE);
    return atMostOnePeakWindowNmOption;
  }

  public static Option createMinPeakForceRelativeToEmpiricalBaselinePnOption() {
    Option minPeakForceRelativeToEmpiricalBaselinePnOption = new Option(
      "minPeakForceRelativeToEmpiricalBaselinePn", true,
      "Can be slightly larger than the minimum peak force.");
    minPeakForceRelativeToEmpiricalBaselinePnOption.setRequired(true);
    minPeakForceRelativeToEmpiricalBaselinePnOption.setType(Double.TYPE);
    return minPeakForceRelativeToEmpiricalBaselinePnOption;
  }

  public static Option createMedianAbsoluteDeviationLowerBoundPnOption() {
    Option medianAbsoluteDeviationLowerBoundPnOption = new Option(
      "medianAbsoluteDeviationLowerBoundPn", true, String.format(
        Locale.ROOT, "Should be around %s.",
        PicoDataAnalyser.defaultMedianAbsoluteDeviationLowerBoundN * 1e12));
    medianAbsoluteDeviationLowerBoundPnOption.setRequired(false);
    medianAbsoluteDeviationLowerBoundPnOption.setType(Double.TYPE);
    return medianAbsoluteDeviationLowerBoundPnOption;
  }

  public static Option createLeftMostlyHigherWindowSizeNmOption() {
    Option option = new Option("leftMostlyHigherWindowSizeNm", true, String.format(
      Locale.ROOT, "Should be around %s.",
      PicoDataAnalyser.defaultLeftMostlyHigherWindowSizeM * 1e9));
    option.setRequired(true);
    option.setType(Double.TYPE);
    return option;
  }

  public static Option createLeftMostlyHigherFractionOption() {
    Option option = new Option("leftMostlyHigherFraction", true, String.format(
      Locale.ROOT, "Should be around %s.", PicoDataAnalyser.defaultLeftMostlyHigherFraction));
    option.setRequired(true);
    option.setType(Double.TYPE);
    return option;
  }

  public static Option createLeftMostlyHigherPnOption() {
    Option option = new Option("leftMostlyHigherPn", true, String.format(
      Locale.ROOT, "Should be around %s.",
      PicoDataAnalyser.defaultLeftMostlyHigherN * 1e12));
    option.setRequired(true);
    option.setType(Double.TYPE);
    return option;
  }

  public static Option createRightMuchHigherThanAllPnOption() {
    Option option = new Option("rightMuchHigherThanAllPn", true, String.format(
      Locale.ROOT, "Should be around %s.", PicoDataAnalyser.defaultRightMuchHigherThanAllN * 1e12));
    option.setRequired(false);
    option.setType(Double.TYPE);
    return option;
  }

  public static Option createRightMuchHigherPnOption() {
    Option option = new Option("rightMuchHigherPn", true, String.format(
      Locale.ROOT, "Should be around %s.", PicoDataAnalyser.defaultRightMuchHigherN * 1e12));
    option.setRequired(false);
    option.setType(Double.TYPE);
    return option;
  }

  public String getSvgDir(CommandLine line) {
    return line.getOptionValue(svgDirOption.getOpt());
  }

  public String getInfoDir(CommandLine line) {
    return line.getOptionValue(infoDirOption.getOpt());
  }

  public Function<? super Double, Double> minPeakForceN(CommandLine line) {
    return minPeakForceN(line, minPeakForcePnOption);
  }

  public double minPeakContourLengthM(CommandLine line) {
    return minPeakContourLengthM(line, minPeakContourLengthNmOption);
  }

  public double minContourLengthDeltaM(CommandLine line) {
    return minContourLengthDeltaM(line, minContourLengthDeltaNmOption);
  }

  public double minPeakForceRelativeToEmpiricalBaselineN(CommandLine line) {
    return minPeakForceRelativeToEmpiricalBaselineN(
      line, minPeakForceRelativeToEmpiricalBaselinePnOption);
  }

  public Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN(CommandLine line) {
    return medianAbsoluteDeviationLowerBoundN(line, medianAbsoluteDeviationLowerBoundPnOption);
  }

  public double leftMostlyHigherWindowSizeM(CommandLine line) {
    return leftMostlyHigherWindowSizeM(line, leftMostlyHigherWindowSizeNmOption);
  }

  public double leftMostlyHigherFraction(CommandLine line) {
    return leftMostlyHigherFraction(line, leftMostlyHigherFractionOption);
  }

  public double leftMostlyHigherN(CommandLine line) {
    return leftMostlyHigherN(line, leftMostlyHigherPnOption);
  }

  public Function<? super Double, Double> rightMuchHigherThanAllN(CommandLine line) {
    return rightMuchHigherThanAllN(line, rightMuchHigherThanAllPnOption);
  }

  public Function<? super Double, Double> rightMuchHigherN(CommandLine line) {
    return rightMuchHigherN(line, rightMuchHigherPnOption);
  }

  public double atMostOnePeakWindowM(CommandLine line) {
    return atMostOnePeakWindowM(line, atMostOnePeakWindowNmOption);
  }

  public static double leftMostlyHigherWindowSizeM(CommandLine line, Option option) {
    return Double.parseDouble(line.getOptionValue(option.getOpt())) / 1e9;
  }
  
  public static DoubleList allowedTimeRelativeError(
    CommandLine line, Option option) {
    String s = line.getOptionValue(option.getOpt());
    String[] a = StringUtils.split(StringUtils.trimToEmpty(s), ", ");
    double[] d = new double[a.length];
    for (int i = a.length; --i >= 0; ) {
      d[i] = Double.parseDouble(a[i]);
    }
    return DoubleLists.unmodifiable(DoubleArrayList.wrap(d));
  }

  public static DoubleList refoldAllowedTimeRelativeError(
    CommandLine line, Option option) {
    return allowedTimeRelativeError(line, option);
  }

  public static double leftMostlyHigherFraction(CommandLine line, Option option) {
    return Double.parseDouble(line.getOptionValue(option.getOpt()));
  }

  public static double leftMostlyHigherN(CommandLine line, Option option) {
    return Double.parseDouble(line.getOptionValue(option.getOpt())) / 1e12;
  }

  public static double minPeakContourLengthM(CommandLine line, Option minPeakContourLengthNmOption) {
    return Double.parseDouble(line.getOptionValue(minPeakContourLengthNmOption.getOpt())) / 1e9;
  }

  public static double minContourLengthDeltaM(CommandLine line, Option minContourLengthDeltaNmOption) {
    return Double.parseDouble(line.getOptionValue(minContourLengthDeltaNmOption.getOpt())) / 1e9;
  }

  public static double atMostOnePeakWindowM(CommandLine line, Option atMostOnePeakWindowNmOption) {
    return Double.parseDouble(line.getOptionValue(atMostOnePeakWindowNmOption.getOpt())) / 1e9;
  };

  public static double minPeakForceRelativeToEmpiricalBaselineN(
    CommandLine line, Option minPeakForceRelativeToEmpiricalBaselinePnOption) {
    return Double.parseDouble(line.getOptionValue(minPeakForceRelativeToEmpiricalBaselinePnOption.getOpt())) / 1e12;
  }

  public static Function<? super Double, Double> medianAbsoluteDeviationLowerBoundN(
    CommandLine line, Option option) {
    return nmPerSExpToPnOptionToMPerSToNFunction(
      line, option, PicoDataAnalyser.defaultMedianAbsoluteDeviationLowerBoundN);
  }

  public static Function<? super Double, Double> nmPerSExpToPnOptionToMPerSToNFunction(
    CommandLine line, Option option, double defaultValue) {
    String optionValue = line.getOptionValue(option.getOpt());
    if (StringUtils.isEmpty(optionValue)) {
      return Functions.constant(defaultValue);
    } else if (CommonThings.fpPattern.matcher(optionValue).matches()) {
      return Functions.constant(Double.parseDouble(optionValue) / 1e12);
    } else {
      Function<Double, Double> nmPerSToPn = LinearFunction.parseTwoPointsStringLogX(optionValue);
      return Functions.compose(
        new MulFunction(1e-12),
        Functions.compose(
          nmPerSToPn, Functions.compose(new LogFunction(Math.E), new MulFunction(1e9))));
    }
  }

  public static Function<? super Double, Double> rightMuchHigherThanAllN(CommandLine line, Option option) {
    return nmPerSExpToPnOptionToMPerSToNFunction(
      line, option, PicoDataAnalyser.defaultRightMuchHigherThanAllN);
  }

  public static Function<? super Double, Double> rightMuchHigherN(CommandLine line, Option option) {
    return nmPerSExpToPnOptionToMPerSToNFunction(
      line, option, PicoDataAnalyser.defaultRightMuchHigherN);
  }

  public static Function<? super Double, Double> minPeakForceN(CommandLine line, Option option) {
    return nmPerSExpToPnOptionToMPerSToNFunction(
      line, option, PicoDataAnalyser.defaultMinimumPeakForceN);
  }

  public double getLinkerNm(CommandLine line) {
    return Double.parseDouble(line.getOptionValue(linkerNmOption.getOpt()));
  }

  public double getLinkerPerModuleNm(CommandLine line) {
    return Double.parseDouble(line.getOptionValue(linkerPerModuleNmOption.getOpt()));
  }

  public int getMaxRequiredWellSpacedAtEnd(CommandLine line) {
    return Integer.parseInt(line.getOptionValue(maxRequiredWellSpacedAtEndOption.getOpt()));
  }

  public double linkerM(CommandLine line) {
    return getLinkerNm(line) / 1e9;
  }

  public double linkerPerModuleM(CommandLine line) {
    return getLinkerPerModuleNm(line) / 1e9;
  }

  public double getTemperatureK(CommandLine line) {
    return temperatureK(line, temperatureKOption);
  }

  public static double temperatureK(CommandLine line, Option temperatureKOption) {
    return Double.parseDouble(line.getOptionValue(temperatureKOption.getOpt()));
  }

  public double getPersistenceLengthNm(CommandLine line) {
    return getPersistenceLengthNm(line, persistenceLengthNmOption);
  }

  public double persistenceLengthM(CommandLine line) {
    return persistenceLengthM(line, persistenceLengthNmOption);
  }

  public String getFetchRefoldBreakParams(CommandLine line) {
    return line.getOptionValue(fetchRefoldBreakParamsOption.getOpt());
  }

  public FetchRefoldBreakParams fetchRefoldBreakParams(CommandLine line) {
    return fetchRefoldBreakParams(line, fetchRefoldBreakParamsOption);
  }

  public static FetchRefoldBreakParams fetchRefoldBreakParams(CommandLine line, Option opt) {
    String params = line.getOptionValue(opt.getOpt());
    if (StringUtils.isEmpty(params)) {
      return null;
    }
    return FetchRefoldBreakParams.parseOrFail(params);
  }

  public static LoopParams loopParams(CommandLine line, Option opt) {
    String params = line.getOptionValue(opt.getOpt());
    if (StringUtils.isEmpty(params)) {
      return null;
    }
    return LoopParams.parseOrFail(params);
  }

  public Path svgDir(CommandLine line) {
    String svgDir = getSvgDir(line);
    return StringUtils.isEmpty(svgDir) ? null : Paths.get(svgDir);
  }

  public Path infoDir(CommandLine line) {
    String infoDir = getInfoDir(line);
    return StringUtils.isEmpty(infoDir) ? null : Paths.get(infoDir);
  }

  public static Option createFileFormatOption() {
    Option fileFormatOption = new Option("fileFormat", true, "nanoscope or jpk");
    fileFormatOption.setRequired(false);
    fileFormatOption.setType(String.class);
    return fileFormatOption;
  }

  public static Option createAllowedTimeRelativeErrorOption() {
    return new Option("allowedTimeRelativeError", true, "7 comma-separated fractions");
  }

  public static Option createRefoldAllowedTimeRelativeErrorOption() {
    return new Option("refoldAllowedTimeRelativeError", true, "11 comma-separated fractions");
  }

  public static double persistenceLengthM(CommandLine line, Option persistenceLengthNmOption) {
    return getPersistenceLengthNm(line, persistenceLengthNmOption) / 1e9;
  }

  public static double getPersistenceLengthNm(CommandLine line, Option persistenceLengthNmOption) {
    return Double.parseDouble(line.getOptionValue(persistenceLengthNmOption.getOpt()));
  }

  private static class LogFunction implements Function<Double, Double> {

    private final double logBase;

    LogFunction(double base) {
      this.logBase = Math.log(base);
    }

    @Override
    public Double apply(Double input) {
      return Math.log(input) / logBase;
    }
  };

  private static class MulFunction implements Function<Double, Double> {

    private final double factor;

    MulFunction(double factor) {
      this.factor = factor;
    }

    @Override
    public Double apply(Double input) {
      return input * factor;
    }
  }
}
